package pod

import (
	"context"
	"fmt"
	v1 "k8s.io/api/core/v1"
	v13 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/rand"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
	appv1beta1 "taptap.com/app/api/v1beta1"
	"time"
)

const (
	K8S_NAMESPACE = "K8S_NAMESPACE"
	POD_NAME      = "POD_NAME"
	POD_IP        = "POD_IP"
	NODE_NAME     = "NODE_NAME"
)

// 检查 new pod是否已经ready
// 检查 replicas是否符合期望，少了就创建，多了就删除
// 检查 旧pod是否有异常状态， 有的话删除
// 更新 旧pod, 按照UpdateStrategy type 分为, Recreate, Rolling, Inplace, 并且按照 percent进行升级
func ReconcilePod(ctx context.Context, c client.Client,
	req ctrl.Request, app *appv1beta1.App) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	podList := &v1.PodList{}
	err := c.List(ctx, podList, client.InNamespace(req.Namespace),
		client.MatchingLabels(app.Spec.Selector))
	if err != nil {
		logger.Error(err, "unable to list Pod")
		return ctrl.Result{}, err
	}

	podList.Items = sortPodList(podList, app)
	result, err := ReconcileNewPod(ctx, app, podList)
	if err != nil {
		logger.Error(err, "ReconcileNewPod failed")
		return result, err
	}

	if result.Requeue || result.RequeueAfter > 0 {
		logger.V(1).Info(fmt.Sprintf("ReconcileNewPod:result after:%d",
			result.RequeueAfter/time.Second))
		<-time.After(time.Second * 10)
		return result, nil
	}

	result, err = ReconcilePodReplicas(ctx, c, app, podList)
	if err != nil {
		logger.Error(err, "ReconcilePodReplicas failed")
		return result, err
	}

	if result.Requeue || result.RequeueAfter > 0 {
		logger.V(1).Info(fmt.Sprintf("ReconcilePodReplicas:result after:%d",
			result.RequeueAfter/time.Second))
		<-time.After(time.Second * 10)
		return result, nil
	}

	result, err = ReconcileOldPodErrorStatus(ctx, c, app, podList)
	if err != nil {
		logger.Error(err, "ReconcileOldPodErrorStatus failed")
		return result, err
	}

	if result.Requeue || result.RequeueAfter > 0 {
		logger.V(1).Info(fmt.Sprintf("ReconcileOldPodErrorStatus:result after:%d",
			result.RequeueAfter/time.Second))
		<-time.After(time.Second * 10)
		return result, nil
	}

	result, err = ReconcileOldPod(ctx, c, app, podList)
	if err != nil {
		logger.Error(err, "ReconcileOldPod failed")
		return result, err
	}

	if result.Requeue || result.RequeueAfter > 0 {
		logger.V(1).Info(fmt.Sprintf("ReconcileOldPod:result after:%d",
			result.RequeueAfter/time.Second))
		<-time.After(time.Second * 10)
		return result, nil
	}

	return ctrl.Result{}, nil
}

func initDefaultUpdateStrategy(app *appv1beta1.App) {
	defaultPercent := 25
	if app.Spec.UpdateStrategy == nil {
		app.Spec.UpdateStrategy = &appv1beta1.AppUpdateStrategy{
			Percent: &defaultPercent,
		}
	}

	if app.Spec.UpdateStrategy.Percent == nil || *app.Spec.UpdateStrategy.Percent == 0 {
		app.Spec.UpdateStrategy.Percent = &defaultPercent
	}
}

func ReconcileOldPod(ctx context.Context, c client.Client,
	app *appv1beta1.App, podList *v1.PodList) (ctrl.Result, error) {

	podListLength := len(podList.Items)
	if podListLength == 0 {
		return ctrl.Result{}, nil
	}

	initDefaultUpdateStrategy(app)

	needReconcilePod := 0
	podStep := app.Spec.Replicas**app.Spec.UpdateStrategy.Percent/100 + 1
	if app.Spec.Replicas**app.Spec.UpdateStrategy.Percent%100 == 0 {
		podStep = app.Spec.Replicas * *app.Spec.UpdateStrategy.Percent / 100
	}
	for _, pod := range podList.Items {
		if pod.Spec.Containers[0].Image == app.Spec.Image {
			break
		}

		if needReconcilePod >= podStep {
			break
		}

		needReconcilePod++
	}

	if needReconcilePod == 0 {
		return ctrl.Result{}, nil
	}

	type StrategyFn struct {
		Match func(app *appv1beta1.App) bool
		Fn    func(context.Context, client.Client, *appv1beta1.App, int, *v1.PodList) (ctrl.Result, error)
	}
	strategyFns := []StrategyFn{
		{
			Match: func(app *appv1beta1.App) bool {
				return app.Spec.UpdateStrategy != nil &&
					app.Spec.UpdateStrategy.Type == appv1beta1.AppUpdateStrategyTypeInPlace
			},
			Fn: ReconcileOldPodInPlace,
		},
		{
			Match: func(app *appv1beta1.App) bool {
				return app.Spec.UpdateStrategy != nil &&
					app.Spec.UpdateStrategy.Type == appv1beta1.AppUpdateStrategyTypeReCreate
			},
			Fn: ReconcileOldPodRecreate,
		},
		{
			Match: func(app *appv1beta1.App) bool {
				return true
			},
			Fn: ReconcileOldPodRollingUpdate,
		},
	}

	for _, strategyFn := range strategyFns {
		if !strategyFn.Match(app) {
			continue
		}

		return strategyFn.Fn(ctx, c, app, needReconcilePod, podList)
	}

	return ctrl.Result{}, nil
}

func ReconcileOldPodRollingUpdate(ctx context.Context, c client.Client,
	app *appv1beta1.App, needReconcilePod int, podList *v1.PodList) (ctrl.Result, error) {

	logger := log.FromContext(ctx)
	podListLength := len(podList.Items)
	gap := podListLength - app.Spec.Replicas
	if gap < needReconcilePod {
		for gap < needReconcilePod {
			err := c.Create(ctx, newPod(app))
			if err != nil {
				logger.Error(err, "ReconcileOldPodRecreate:create pod failed")
				return ctrl.Result{}, err
			}
			gap++
		}
	} else {
		for i := 0; i < gap; i++ {
			err := c.Delete(ctx, &podList.Items[i])
			if err != nil {
				logger.Error(err, "ReconcileOldPodRecreate:delete pod failed")
				return ctrl.Result{}, err
			}
		}
	}

	return ctrl.Result{
		Requeue: true,
	}, nil
}

func ReconcileOldPodRecreate(ctx context.Context, c client.Client,
	app *appv1beta1.App, needReconcilePod int, podList *v1.PodList) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	for i := 0; i < needReconcilePod; i++ {
		err := c.Create(ctx, newPod(app))
		if err != nil {
			logger.Error(err, "ReconcileOldPodRecreate:create pod failed")
			return ctrl.Result{}, err
		}

		err = c.Delete(ctx, &podList.Items[i])
		if err != nil {
			logger.Error(err, "ReconcileOldPodRecreate:delete pod failed")
			return ctrl.Result{}, err
		}
	}
	return ctrl.Result{
		Requeue: true,
	}, nil
}

func ReconcileOldPodInPlace(ctx context.Context, c client.Client,
	app *appv1beta1.App, needReconcilePod int, podList *v1.PodList) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	for i := 0; i < needReconcilePod; i++ {
		pod := podList.Items[i]
		pod.Spec.Containers[0].Resources = app.Spec.Resources
		pod.Spec.Containers[0].Image = app.Spec.Image
		err := c.Update(ctx, &pod)
		if err != nil {
			logger.Error(err, "ReconcileOldPodInPlace:update pod failed")
			return ctrl.Result{}, err
		}
	}
	return ctrl.Result{
		Requeue: true,
	}, nil
}

func ReconcileOldPodErrorStatus(ctx context.Context, c client.Client,
	app *appv1beta1.App, podList *v1.PodList) (ctrl.Result, error) {

	podListLength := len(podList.Items)
	if podListLength == 0 {
		return ctrl.Result{}, nil
	}

	logger := log.FromContext(ctx)
	reconcile := false
	for _, pod := range podList.Items {
		if pod.Spec.Containers[0].Image == app.Spec.Image {
			break
		}

		if pod.Status.Phase == v1.PodRunning {
			break
		}

		err := c.Delete(ctx, &pod)
		if err != nil {
			logger.Error(err, "ReconcileOldPodErrorStatus:delete pod failed")
			return ctrl.Result{}, err
		}
		reconcile = true
	}

	if reconcile {
		return ctrl.Result{
			Requeue: true,
		}, nil
	}

	return ctrl.Result{}, nil
}

func ReconcilePodReplicas(ctx context.Context, c client.Client,
	app *appv1beta1.App, podList *v1.PodList) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	podListLength := len(podList.Items)
	if app.Spec.Replicas == podListLength {
		return ctrl.Result{}, nil
	}

	gap := app.Spec.Replicas - podListLength
	if gap > 0 {
		for gap > 0 {
			pod := newPod(app)
			err := c.Create(ctx, pod)
			if err != nil {
				logger.Error(err, "create pod failed")
				return ctrl.Result{}, err
			}
			gap--
		}

		return ctrl.Result{
			Requeue: true,
		}, nil
	}

	gap = 0 - gap
	for i := 0; i < gap; i++ {
		pod := podList.Items[i]
		err := c.Delete(ctx, &pod)
		if err != nil {
			logger.Error(err, "delete pod failed")
			return ctrl.Result{}, err
		}
	}

	return ctrl.Result{
		Requeue: true,
	}, nil
}

func ReconcileNewPod(ctx context.Context, app *appv1beta1.App, podList *v1.PodList) (ctrl.Result, error) {
	podListLength := len(podList.Items)
	if podListLength == 0 {
		return ctrl.Result{}, nil
	}

	for _, pod := range podList.Items {
		if pod.Spec.Containers[0].Image != app.Spec.Image {
			continue
		}

		if pod.Status.Phase != v1.PodRunning {
			return ctrl.Result{
				Requeue: true,
			}, nil
		}

		for _, status := range pod.Status.ContainerStatuses {
			if !status.Ready {
				return ctrl.Result{
					Requeue: true,
				}, nil
			}
		}
	}

	return ctrl.Result{}, nil
}

// old failed - old unknown - old pending - new failed - new unknown - new pending - old running - new running
func sortPodList(podList *v1.PodList, app *appv1beta1.App) []v1.Pod {
	newPods := make(map[string][]v1.Pod)
	oldPods := make(map[string][]v1.Pod)
	fn := func(pod v1.Pod, pods map[string][]v1.Pod) map[string][]v1.Pod {
		pods[string(pod.Status.Phase)] = append(pods[string(pod.Status.Phase)], pod)
		return pods
	}

	for _, pod := range podList.Items {
		if pod.Spec.Containers[0].Image == app.Spec.Image {
			newPods = fn(pod, newPods)
			continue
		}

		oldPods = fn(pod, oldPods)
	}

	pods := make([]v1.Pod, 0)
	pods = append(pods, oldPods[string(v1.PodFailed)]...)
	pods = append(pods, oldPods[string(v1.PodUnknown)]...)
	pods = append(pods, oldPods[string(v1.PodPending)]...)
	pods = append(pods, newPods[string(v1.PodFailed)]...)
	pods = append(pods, newPods[string(v1.PodUnknown)]...)
	pods = append(pods, newPods[string(v1.PodPending)]...)
	pods = append(pods, oldPods[string(v1.PodRunning)]...)
	pods = append(pods, newPods[string(v1.PodRunning)]...)
	return pods
}

func newPod(app *appv1beta1.App) *v1.Pod {
	return &v1.Pod{
		ObjectMeta: v13.ObjectMeta{
			Name:      fmt.Sprintf("%s-%s", app.Name, rand.String(5)),
			Namespace: app.Namespace,
			Labels:    app.Spec.Selector,
			OwnerReferences: []v13.OwnerReference{
				{
					APIVersion: app.APIVersion,
					Kind:       app.Kind,
					Name:       app.Name,
					UID:        app.UID,
				},
			},
		},

		Spec: v1.PodSpec{
			Containers: []v1.Container{
				{
					Name:      "main",
					Image:     app.Spec.Image,
					Resources: app.Spec.Resources,
					Env: []v1.EnvVar{
						{
							Name: K8S_NAMESPACE,
							ValueFrom: &v1.EnvVarSource{
								FieldRef: &v1.ObjectFieldSelector{
									FieldPath: "metadata.namespace",
								},
							},
						},
						{
							Name: POD_NAME,
							ValueFrom: &v1.EnvVarSource{
								FieldRef: &v1.ObjectFieldSelector{
									FieldPath: "metadata.name",
								},
							},
						},
						{
							Name: POD_IP,
							ValueFrom: &v1.EnvVarSource{
								FieldRef: &v1.ObjectFieldSelector{
									FieldPath: "status.podIP",
								},
							},
						},
						{
							Name: NODE_NAME,
							ValueFrom: &v1.EnvVarSource{
								FieldRef: &v1.ObjectFieldSelector{
									FieldPath: "status.hostIP",
								},
							},
						},
					},
				},
			},
		},
	}
}
